/*
* Copyright (c) 2011 Connection Road.  All rights reserved.
* This program and the accompanying material are made available
* under the terms of the Eclipse Public Licnese v1.0 which is
* available at http://www.eclipse.org/legal/epl-v10.html
*/

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml.Serialization;
using SyncToy;

namespace CrSyncFilesUtils
{

    /// <summary>
    /// Manages CRSyncFiles folder pair database.
    /// </summary>
    public partial class FileSyncPairManager
    {


        public FileSyncPairManager(string storageFile)
        {
            StorageFileName = storageFile;
        }

        /// <summary>
        /// Sets and gets the file that contains the saved folder pairs
        /// </summary>
        public string StorageFileName { get; set; }

        /// <summary>
        /// Use this property to get the current folder pair
        /// </summary>
        public FileSyncPairInfo CurrentFolderPair { get; set; }

        /// <summary>
        /// The current folder pair index from the lists of folder pairs
        /// </summary>
        public int CurrentFolderPairIndex { get; set; }


        /// <summary>
        /// Generic message. Use this property to get various information results.
        /// </summary>
        public string Message { get; set; }


        /// <summary>
        /// Gets all available folder pairs.
        /// Throws ApplicationException if an error occurs.
        /// </summary>
        /// <exception cref="IOException"></exception>
        /// <returns>Dictionary object using UniqueIdentifier as the key</returns>
        public Dictionary<string, FileSyncPairInfo> AvailableFolderPairs()
        {
            var pairDocument = GetFileSyncPairDocumentInfo();
            return pairDocument.GetFolderPairs();
        }


        /// <summary>
        /// Updates folder pair database
        /// </summary>
        /// <exception cref="Exception"></exception>
        /// <returns></returns>
        public bool UpdateFolderPairs(Dictionary<string, FileSyncPairInfo> folderPairs)
        {
            var pairDocument = GetFileSyncPairDocumentInfo();
            pairDocument.SetFolderPairs(folderPairs);
            return SetFileSyncPairDocumentInfo(pairDocument);
        }

        public bool UpdateFolderPairs(List<FileSyncPairInfo> folderPairs)
        {
            var availableFolderPairs = new Dictionary<string, FileSyncPairInfo>();
            foreach (var folderPair in folderPairs)
            {
                availableFolderPairs[folderPair.UniqueIdentifier] = folderPair;
            }

            return UpdateFolderPairs(availableFolderPairs);
        }


        /// <summary>
        /// Updates the lists of folder pairs in the database with a given name and folder pair object.
        /// </summary>
        /// <param name="folderPair">A FileSyncPairInfo object</param>
        /// <returns></returns>
        public bool UpdateFolderPair(FileSyncPairInfo folderPair)
        {
            if (HasFolderPair(folderPair))
            {
                var availableFolderPairs = AvailableFolderPairs();
                folderPair.ExtendedInfo.AddObjectForKey(DateTime.Now, "UPDATED");
                availableFolderPairs[folderPair.UniqueIdentifier] = folderPair;

                return UpdateFolderPairs(availableFolderPairs);
            }
            return false;
        }

        /// <summary>
        /// Update the folder pair last rund date time stamp.
        /// </summary>
        /// <param name="folderPair"></param>
        /// <param name="dateTime"></param>
        /// <returns></returns>
        public bool UpdateFolderPair(FileSyncPairInfo folderPair, DateTime dateTime)
        {
            CurrentFolderPair.DateTimeLastSync = dateTime;
            var availableFolderPairs = AvailableFolderPairs();
            folderPair.ExtendedInfo.AddObjectForKey(DateTime.Now, "UPDATED");
            availableFolderPairs[folderPair.UniqueIdentifier] = folderPair;

            return UpdateFolderPairs(availableFolderPairs);
        }


        /// <summary>
        /// Adds folder pair.
        /// 
        /// </summary>
        /// <param name="aPair"></param>
        /// <returns>CommonResult.Exists | CommonResult.SaveSuccessful | CommonResult.SaveFailed</returns>
        public CrSyncFilesResult AddFolderPair(FileSyncPairInfo aPair)
        {
            if (HasFolderPairNamed(aPair.FolderPairName))
            {
                Message = "Folder pair already exists.";
                return CrSyncFilesResult.Found;
            }

            var availableFolderPairs = AvailableFolderPairs();
            availableFolderPairs.Add(aPair.UniqueIdentifier, aPair);

            if (UpdateFolderPairs(availableFolderPairs))
            {
                aPair.CreateMarkerFiles();
                return CrSyncFilesResult.SaveSuccessful;
            }
            return CrSyncFilesResult.SaveFailed;
        }

        public bool HasFolderPairNamed(string folderPairName)
        {
            var availableFolderPairs = AvailableFolderPairs();

            foreach (var pkv in availableFolderPairs.Where(pkv => pkv.Value.FolderPairName.ToUpper() == folderPairName.ToUpper()))
            {
                CurrentFolderPair = pkv.Value;
                return true;
            }
            return false;
        }

        /// <summary>
        /// Checks to see if a folder pair with a given name exists.
        /// </summary>
        /// <returns></returns>
        public bool HasFolderPair(FileSyncPairInfo folderPair)
        {
            var availableFolderPairs = AvailableFolderPairs();
            if (availableFolderPairs.ContainsKey(folderPair.UniqueIdentifier))
            {
                CurrentFolderPair = availableFolderPairs[folderPair.UniqueIdentifier];
                return true;
            }
            return false;
        }


        public bool HasFolderPair(SyncEngineConfig synctoyPair)
        {
            var availableFolderPairs = AvailableFolderPairs();
            foreach (var pair in availableFolderPairs.Where(pair => pair.Value.IsIdenticalWith(synctoyPair)))
            {
                CurrentFolderPair = pair.Value;
                return true;
            }
            return false;
        }




        /// <summary>
        /// Removes a folder pair with a given name
        /// </summary>
        /// <param name="aName"></param>
        /// <returns></returns>
        public bool RemoveFolderPairNamed(string aName)
        {
            if (HasFolderPairNamed(aName))
            {
                var availableFolderPairs = AvailableFolderPairs();
                try
                {

                    // Remove metadata
                    RemoveMetadata(CurrentFolderPair);

                    // Remove from the list
                    availableFolderPairs.Remove(CurrentFolderPair.UniqueIdentifier);

                    return UpdateFolderPairs(availableFolderPairs);
                }
                catch (ArgumentOutOfRangeException exception)
                {
                    Message = exception.Message;
                    return false;
                }
            }
            Message = "No folder pair with that name exists.";
            return false;
        }

        public bool RemoveFolderPair(FileSyncPairInfo folderPair)
        {
            if (HasFolderPair(folderPair))
            {
                // Remove metadata
                RemoveMetadata(folderPair);

                var availableFolderPairs = AvailableFolderPairs();
                availableFolderPairs.Remove(folderPair.UniqueIdentifier);

                return UpdateFolderPairs(availableFolderPairs);
            }
            return false;
        }

        /// <summary>
        /// Rename the folder pair with a given new name
        /// </summary>
        /// <param name="oldName"></param>
        /// <param name="newName"></param>
        /// <returns></returns>
        public bool RenameFolderPair(string oldName, string newName)
        {
            if (HasFolderPairNamed(oldName))
            {
                if (HasFolderPairNamed(newName))
                {
                    return false;
                }
                CurrentFolderPair.FolderPairName = newName;
                CurrentFolderPair.ExtendedInfo.AddObjectForKey(DateTime.Now, "UPDATED");
                var availableFolderPairs = AvailableFolderPairs();
                availableFolderPairs[CurrentFolderPair.UniqueIdentifier] = CurrentFolderPair;

                return UpdateFolderPairs(availableFolderPairs);
            }
            return false;
        }

        public bool RenameFolderPair(FileSyncPairInfo folderPair, string newName)
        {
            if (HasFolderPairNamed(newName))
            {
                return false;
            }
            folderPair.FolderPairName = newName;
            folderPair.ExtendedInfo.AddObjectForKey(DateTime.Now, "UPDATED");
            var availableFolderPairs = AvailableFolderPairs();
            availableFolderPairs[folderPair.UniqueIdentifier] = folderPair;

            return UpdateFolderPairs(availableFolderPairs);
        }

        /// <summary>
        /// Looks for a folder pair with a given unique identifier.
        /// </summary>
        /// <param name="identity">The pair's unique identifier not the folder uid</param>
        /// <returns></returns>
        public bool HasFolderPairWithIdentifier(string identity)
        {
            var availablePairs = AvailableFolderPairs();

            if (availablePairs.ContainsKey(identity))
            {
                CurrentFolderPair = availablePairs[identity];
                return true;
            }

            return false;
        }

        /// <summary>
        /// Looks for a folder pair with a given location's unique identifer.
        /// </summary>
        /// <param name="identity">The left or right's unique identifier not the pair uid</param>
        /// <returns>Either the left or the right folder</returns>
        public bool HasFolderPairLocationWithUid(string identity)
        {
            var availablePairs = AvailableFolderPairs();

            foreach (var pairKv in
                availablePairs.Where(pairKv => (pairKv.Value.LeftFolder.UniqueIdentifier == identity) || (pairKv.Value.RightFolder.UniqueIdentifier == identity)))
            {
                CurrentFolderPair = pairKv.Value;
                return true;
            }

            return false;
        }

        public void ImportSyncToyFolderPairs(List<SyncEngineConfig> synctoyPairs, string prefix, string metaDataDirectory)
        {
            var availableFolderPairs = AvailableFolderPairs();
            foreach (var synctoyPair in synctoyPairs)
            {

                // Make sure no folder pair of the same name exists
                if (!HasFolderPairNamed(synctoyPair.Name))
                {
                    var newFolderPair = CrSyncFilesUtility.ConvertToCrSyncFile(synctoyPair, prefix, metaDataDirectory);

                    // Add the new folder pair to be updated
                    availableFolderPairs.Add(newFolderPair.UniqueIdentifier, newFolderPair);
                }
            }

            UpdateFolderPairs(availableFolderPairs);

        }


        /// <summary>
        /// Creates a folder pair with a unique identifier.
        /// 
        /// </summary>
        /// <param name="name"></param>
        /// <param name="uid">Must be unique.</param>
        /// <returns></returns>
        public static FileSyncPairInfo CreateFolderPair(string name, string uid)
        {
            var newPairInfo = new FileSyncPairInfo(name, uid);
            newPairInfo.ExtendedInfo.AddObjectForKey(DateTime.Now, "CREATED");
            newPairInfo.FileNameIncludes.Add("*");
            return newPairInfo;
        }


        /// <summary>
        /// Creates a folder pair witha unique identifier using Guid.
        /// </summary>
        /// <returns></returns>
        public static FileSyncPairInfo CreateFolderPair(string name)
        {
            var guid = Guid.NewGuid();
            return CreateFolderPair(name, guid.ToString());
        }

        public static FileSyncPairInfo CreateFolderPair()
        {
            return CreateFolderPair(null);
        }



        /// <summary>
        /// Deletes the meta data and various markers
        /// </summary>
        /// <param name="folderPair"></param>
        private void RemoveMetadata(FileSyncPairInfo folderPair)
        {
            
            var leftMetaFile = new FileNode(folderPair.LeftFolder.MetaFilePath);
            var rightMetaFile = new FileNode(folderPair.RightFolder.MetaFilePath);

            var leftMarker = new FileNode(folderPair.LeftFolder.MarkerFilePath);
            var rightMarker = new FileNode(folderPair.RightFolder.MarkerFilePath);

            leftMetaFile.Remove();
            rightMetaFile.Remove();

            leftMarker.Remove();
            rightMarker.Remove();
            
        }


        /// <summary>
        /// Retrieves CRSyncFiles' storage information represented as FileSyncPairDocumentInfo object.
        /// </summary>
        /// <returns></returns>
        public FileSyncPairDocumentInfo GetFileSyncPairDocumentInfo()
        {
            if (File.Exists(StorageFileName))
            {
                Stream fileStream = new FileStream(StorageFileName, FileMode.Open);
                var xmlSerializer = new XmlSerializer(typeof(FileSyncPairDocumentInfo));
                var pairDocument = (FileSyncPairDocumentInfo)xmlSerializer.Deserialize(fileStream);
                fileStream.Close();
                return pairDocument;
            }
            return new FileSyncPairDocumentInfo();
        }

        /// <summary>
        /// Sets the FileSyncPairDocumentInfo object and store it on disk.
        /// </summary>
        /// <param name="document"></param>
        /// <returns></returns>
        public bool SetFileSyncPairDocumentInfo(FileSyncPairDocumentInfo document)
        {
            try
            {
                if (File.Exists(StorageFileName))
                {
                    var newFileName = Path.ChangeExtension(StorageFileName, ".bak");
                    File.Copy(StorageFileName, newFileName, true);
                }
                Stream fileStream = File.Open(StorageFileName, FileMode.Create);
                var xmlSerializer = new XmlSerializer(typeof(FileSyncPairDocumentInfo));
                xmlSerializer.Serialize(fileStream, document);
                fileStream.Close();
                Message = "";
                return true;
            }
            catch (Exception pairException)
            {
                Message = pairException.Message;
                throw;
            }
        }

        public FileSyncPairDocumentInfo CrSyncDocumentInfo
        {
            get { return GetFileSyncPairDocumentInfo(); }
            set { SetFileSyncPairDocumentInfo(value); }
        }


        /// <summary>
        /// Loops through all folder pairs and update the reference to the directory where meta data should be saved.
        /// </summary>
        /// <param name="metadataDirectory"></param>
        public void UpdateMetaDataDiretory(string metadataDirectory)
        {
            var folderPairs = AvailableFolderPairs();
            foreach (var folderPair in folderPairs)
            {
                folderPair.Value.LeftFolder.MetaDataDirectory = metadataDirectory;
                folderPair.Value.RightFolder.MetaDataDirectory = metadataDirectory;
            }

            UpdateFolderPairs(folderPairs);
        }

        /// <summary>
        /// Creates an approriate CrSyncFileLocation object based on the given absolute path.
        /// NOTE: Modify and add your location object construction here.
        /// </summary>
        /// <param name="driveType"></param>
        /// <param name="path"></param>
        /// <param name="prefix"></param>
        /// <param name="uid"></param>
        /// <param name="metaDataDirectory"></param>
        /// <returns></returns>
        public static ICrSyncLocation CreateFileLocation(DriveType? driveType, string path, string prefix, string uid, string metaDataDirectory)
        {
            
            // Perform override if necessary
            try
            {
                var pairManagerType = typeof (FileSyncPairManager);
                var arguments = new object[] {driveType, path, prefix, uid, metaDataDirectory};
                var createdLocation = pairManagerType.InvokeMember("OverrideCreateFileLocation",
                                                                   BindingFlags.Default | BindingFlags.InvokeMethod,
                                                                   null, null, arguments);
                return (ICrSyncLocation) createdLocation;
            }
            catch (MissingMethodException)
            {
                var fileNode = new FileNode(path);
                if (fileNode.IsUnc())
                {
                    return new CrSyncFileLocationNetwork(path, uid, prefix, metaDataDirectory);
                }

                if (driveType != null)
                {
                    switch (driveType)
                    {
                        case DriveType.Fixed:
                            return new CrSyncFileLocationLocal(path, uid, prefix, metaDataDirectory);
                        case DriveType.Removable:
                            return new CrSyncFileLocationExternal(path, uid, prefix, metaDataDirectory);
                        default:
                            return new CrSyncFileLocationLocal(path, uid, prefix, metaDataDirectory);
                    }
                }

                // Only if drive type is not specified explicitly check if folder exists else will treat it as external
                if (Directory.Exists(path))
                {
                    var driveInfo = new DriveInfo(fileNode.GetRoothPath());
                    switch (driveInfo.DriveType)
                    {
                        case DriveType.Fixed:
                            return new CrSyncFileLocationLocal(path, uid, prefix, metaDataDirectory);
                        case DriveType.Removable:
                            return new CrSyncFileLocationExternal(path, uid, prefix, metaDataDirectory);
                        default:
                            return new CrSyncFileLocationLocal(path, uid, prefix, metaDataDirectory);
                    }
                }

                return new CrSyncFileLocationExternal(path, uid, prefix, metaDataDirectory);
            }

        }



        public static ICrSyncLocation CreateFileLocation(string path, string prefix, string uid, string metaDataDirectory)
        {
            return CreateFileLocation(null, path, prefix, uid, metaDataDirectory);
        }

        /// <summary>
        /// Same as CreateFileLocation(string path, string prefix, string uid, string metaDataDirectory) except
        /// that you force it to the location type you want.
        /// </summary>
        /// <param name="path"></param>
        /// <param name="prefix"></param>
        /// <param name="uid"></param>
        /// <param name="metaDataDirectory"></param>
        /// <param name="forceLocationType"></param>
        /// <returns></returns>
        public static ICrSyncLocation CreateFileLocation(string path, string prefix, string uid, string  metaDataDirectory, CrSyncFileLocationTypes forceLocationType)
        {
            switch (forceLocationType)
            {
                case CrSyncFileLocationTypes.Auto:
                    return CreateFileLocation(path, prefix, uid, metaDataDirectory);
                case CrSyncFileLocationTypes.External:
                    return new CrSyncFileLocationExternal(path, uid, prefix, metaDataDirectory);
                case CrSyncFileLocationTypes.Local:
                    return new CrSyncFileLocationLocal(path, uid, prefix, metaDataDirectory);
                case CrSyncFileLocationTypes.Network:
                    return new CrSyncFileLocationNetwork(path, uid, prefix, metaDataDirectory);
                default:
                    try
                    {
                        // Calling extensionssss
                        var pairManagerType = typeof(FileSyncPairManager);
                        var arguments = new object[] { path, prefix, uid, metaDataDirectory, forceLocationType };
                        var createdLocation = pairManagerType.InvokeMember("CreateFileLocationExtension",
                            BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, 
                            null, 
                            null, 
                            arguments);
                        return (ICrSyncLocation)createdLocation;
                    }
                    catch (MissingMethodException)
                    {
                        return new CrSyncFileLocationLocal(path, uid, prefix, metaDataDirectory);
                    }
                    
            }
        }

    }
}
